Skip to content

fix(codegen): guard declared string self-append operator (#7841) - #7881

Merged
proggeramlug merged 2 commits into
mainfrom
fix/7841-declared-string-self-append
Aug 11, 2026
Merged

fix(codegen): guard declared string self-append operator (#7841)#7881
proggeramlug merged 2 commits into
mainfrom
fix/7841-declared-string-self-append

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • make the s += rhs string-builder lowering choose concatenation versus numeric addition from the destination's runtime tag, not its erased TypeScript annotation
  • snapshot the compound-assignment lhs before evaluating rhs and avoid pre-coercing rhs on the dynamic arm, so user valueOf / toString hooks run once in spec order
  • retain direct js_string_append for real heap strings, including canonical and boxed string-local modes

Regression coverage

The compiled-program test covers:

  • the report's let c: string = (42 as any); c += 1 (43, type number)
  • object rhs coercion (valueOf once, toString zero times)
  • a declared-string rhs that also holds a number
  • an rhs assignment observing the old compound-assignment lhs
  • a 1,000-iteration honest string builder
  • both default canonical-string lowering and PERRY_CANONICAL_STR_LOCALS=0

Validation

  • cargo test -p perry-codegen — all test binaries passed
  • cargo test -p perry --test issue_7841_declared_string_self_append declared_string_self_append_uses_runtime_values_and_preserves_order -- --exact
  • release reproduction: base 421 string; fix 43 number
  • bash scripts/check_file_size.sh

Quiet M1 mini, 21 paired alternating samples per arm, two compilers against one runtime:

Probe Base median Fix median Delta
canonical s += "ab" 0.030833 s 0.031216 s +1.24%
boxed s += "ab" 0.032962 s 0.031152 s -5.49%
canonical s += number 0.044043 s 0.044368 s +0.74%
boxed s += number 0.044558 s 0.044353 s -0.46%

All benchmark outputs matched in every sample; mini load stayed between 1.51 and 1.73.

Closes #7841

Summary by CodeRabbit

  • Bug Fixes

    • Fixed += behavior for string-annotated variables initialized with numeric values.
    • Runtime values now correctly determine whether to perform string concatenation or numeric addition.
    • Preserved JavaScript coercion semantics, evaluation order, and support for long string concatenation.
  • Tests

    • Added regression coverage for numeric, string, coercion, and long-string self-append scenarios.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Declared-string += lowering now dispatches on runtime NaN-box tags. String values retain optimized append paths, while non-string values use dynamic JavaScript addition. Tests cover numeric, object, coercion, evaluation-order, and repeated-append behavior.

Changes

Declared-string self-append

Layer / File(s) Summary
Runtime-tag dispatch
crates/perry-codegen/src/lower_string_concat.rs
Self-append lowering now applies runtime-tag dispatch to all declared-string locals.
Operand lowering and rooting
crates/perry-codegen/src/lower_string_concat.rs
Heap strings, SSO strings, dynamic values, and non-string operands use separate branches with rooting and operand reloads around collecting operations.
Regression coverage
crates/perry-codegen/tests/shadow_slot_hygiene.rs, crates/perry/tests/issue_7841_declared_string_self_append.rs, changelog.d/7881-declared-string-self-append.md
Tests verify generated helper selection and runtime behavior across canonical and boxed string modes. The changelog records the fix.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DeclaredStringLocal
  participant SelfAppendLowering
  participant RuntimeHelpers
  DeclaredStringLocal->>SelfAppendLowering: load destination and RHS
  SelfAppendLowering->>SelfAppendLowering: inspect runtime NaN-box tags
  SelfAppendLowering->>RuntimeHelpers: call string append or dynamic string-or-number addition
  RuntimeHelpers-->>DeclaredStringLocal: store self-append result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy #7841 by using runtime tags, preserving string append optimization, fixing evaluation order, and covering canonical and boxed paths.
Out of Scope Changes check ✅ Passed The code, tests, and changelog entry are directly related to the declared-string self-append fix and its required regression coverage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title clearly identifies the codegen fix for declared string self-append behavior.
Description check ✅ Passed The description explains the changes, related issue, regression coverage, validation commands, and benchmark results; only the template checklist is omitted.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7841-declared-string-self-append

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug
proggeramlug marked this pull request as ready for review August 11, 2026 18:24

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
crates/perry-codegen/tests/shadow_slot_hygiene.rs (1)

1226-1233: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bind the assertions to the dispatch, not to call presence.

Both arms are always emitted for a non-string rhs, so both contains checks pass even if the tag test is wrong or absent. Reuse the existing block_def_offset helper to assert that js_string_append appears inside the strapp.append block and that js_dynamic_string_or_number_add appears inside the strapp.dynamic block. Also assert that the destination tag comparison against the heap-string tag exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/tests/shadow_slot_hygiene.rs` around lines 1226 - 1233,
Update the assertions in the shadow-slot hygiene test to validate dispatch
placement rather than global call presence. Reuse block_def_offset to assert
js_string_append is within strapp.append and js_dynamic_string_or_number_add is
within strapp.dynamic, and add an assertion that the destination tag comparison
against the heap-string tag is emitted.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-codegen/src/lower_string_concat.rs`:
- Around line 195-202: The lhs snapshot is lost when rhs evaluation mutates the
same local without allocation. In
crates/perry-codegen/src/lower_string_concat.rs:195-202, root the destination
unconditionally on the non-string path and use group.reread_emitted(ctx,
lhs_root) instead of reloading slot; apply the same change in the proven-string
SSO arm at crates/perry-codegen/src/lower_string_concat.rs:295-299, and add a
regression covering a heap-string destination with an rhs assignment to that
same local.

In `@crates/perry/tests/issue_7841_declared_string_self_append.rs`:
- Around line 25-35: Update the compiler environment setup in the test’s
command-building loop to set PERRY_CANONICAL_STR_LOCALS explicitly for both
canonical_strings values: enable canonical locals for the canonical arm and
disable them for the non-canonical arm, preventing inherited environment
settings from affecting either compilation.

---

Nitpick comments:
In `@crates/perry-codegen/tests/shadow_slot_hygiene.rs`:
- Around line 1226-1233: Update the assertions in the shadow-slot hygiene test
to validate dispatch placement rather than global call presence. Reuse
block_def_offset to assert js_string_append is within strapp.append and
js_dynamic_string_or_number_add is within strapp.dynamic, and add an assertion
that the destination tag comparison against the heap-string tag is emitted.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 24081808-d82e-4288-81be-c96ed1220293

📥 Commits

Reviewing files that changed from the base of the PR and between 9ca8b4f and 857d7b0.

📒 Files selected for processing (4)
  • changelog.d/7881-declared-string-self-append.md
  • crates/perry-codegen/src/lower_string_concat.rs
  • crates/perry-codegen/tests/shadow_slot_hygiene.rs
  • crates/perry/tests/issue_7841_declared_string_self_append.rs

Comment on lines +195 to +202
// The coercion can collect. If rhs evaluation needed an old-value
// root, re-read it; otherwise rhs was inert and the local slot is
// still the same value, so reload its GC-updated bits directly.
let lhs_after_coercion = if protect_lhs {
group.reread_emitted(ctx, lhs_root)
} else {
ctx.block().load(DOUBLE, slot)
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

protect_lhs gates the wrong property, so both arms can lose the compound-assignment snapshot. operand_may_collect reports GC risk only. An rhs that stores to the same local without allocating leaves protect_lhs false, and both arms then reload the post-assignment value from slot instead of the snapshot taken before rhs evaluation. let s: string = "<long heap string>"; s += (s = "b") reaches the SSO arm and produces "bb". The supplied order += (order = "new") case does not catch this because both operands are SSO and route to dother, which uses the snapshot register.

  • crates/perry-codegen/src/lower_string_concat.rs#L195-L202: root the destination unconditionally on the non-string path and replace the ctx.block().load(DOUBLE, slot) fallback with group.reread_emitted(ctx, lhs_root).
  • crates/perry-codegen/src/lower_string_concat.rs#L295-L299: apply the same change on the proven-string SSO arm, and add a regression case with a heap-string destination and an rhs that assigns to the same local.
📍 Affects 1 file
  • crates/perry-codegen/src/lower_string_concat.rs#L195-L202 (this comment)
  • crates/perry-codegen/src/lower_string_concat.rs#L295-L299
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/lower_string_concat.rs` around lines 195 - 202, The
lhs snapshot is lost when rhs evaluation mutates the same local without
allocation. In crates/perry-codegen/src/lower_string_concat.rs:195-202, root the
destination unconditionally on the non-string path and use
group.reread_emitted(ctx, lhs_root) instead of reloading slot; apply the same
change in the proven-string SSO arm at
crates/perry-codegen/src/lower_string_concat.rs:295-299, and add a regression
covering a heap-string destination with an rhs assignment to that same local.

Comment on lines +25 to +35
let mut compiler = Command::new(perry_bin());
compiler
.current_dir(dir)
.arg("compile")
.arg(&entry)
.arg("-o")
.arg(&output)
.arg("--no-cache");
if !canonical_strings {
compiler.env("PERRY_CANONICAL_STR_LOCALS", "0");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Set PERRY_CANONICAL_STR_LOCALS explicitly for both arms.

The canonical arm inherits the parent environment. If the environment already sets PERRY_CANONICAL_STR_LOCALS=0, both loop iterations compile the same configuration and the non-canonical lowering is the only one under test. The failure is silent.

🛡️ Proposed fix
-    if !canonical_strings {
-        compiler.env("PERRY_CANONICAL_STR_LOCALS", "0");
-    }
+    compiler.env(
+        "PERRY_CANONICAL_STR_LOCALS",
+        if canonical_strings { "1" } else { "0" },
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let mut compiler = Command::new(perry_bin());
compiler
.current_dir(dir)
.arg("compile")
.arg(&entry)
.arg("-o")
.arg(&output)
.arg("--no-cache");
if !canonical_strings {
compiler.env("PERRY_CANONICAL_STR_LOCALS", "0");
}
let mut compiler = Command::new(perry_bin());
compiler
.current_dir(dir)
.arg("compile")
.arg(&entry)
.arg("-o")
.arg(&output)
.arg("--no-cache");
compiler.env(
"PERRY_CANONICAL_STR_LOCALS",
if canonical_strings { "1" } else { "0" },
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/tests/issue_7841_declared_string_self_append.rs` around lines 25
- 35, Update the compiler environment setup in the test’s command-building loop
to set PERRY_CANONICAL_STR_LOCALS explicitly for both canonical_strings values:
enable canonical locals for the canonical arm and disable them for the
non-canonical arm, preventing inherited environment settings from affecting
either compilation.

@proggeramlug
proggeramlug merged commit cc87d14 into main Aug 11, 2026
1 of 19 checks passed
@proggeramlug
proggeramlug deleted the fix/7841-declared-string-self-append branch August 11, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A declared string local silently mis-lowers s += x: c += 1 on a slot holding 42 gives "421", not 43

1 participant